Replace text aligner with jsdiff - #533
Conversation
leaf-agent
left a comment
There was a problem hiding this comment.
The static import { diffArrays } from "/vendor/jsdiff.esm.js" puts a vendor bundle in the eager boot graph for the first time. text-alignment.js is reached eagerly twice over — leaf.js → runtime/version.js, and leaf.js → composing/selection.js → composing/capture.js for textUnits — and the published site can't fold the bundle away, because worker/bundle-runtime.mjs's preserve-module-urls resolver keeps every /vendor/ path external. So every page fetches jsdiff before presentation whether or not the reader ever opens a draft history or a version comparison. The two ci runs measure it:
| page | main | this PR |
|---|---|---|
/ |
11 JS / 207 KiB | 12 JS / 208 KiB |
/examples/triage-board/ |
10 JS / 181 KiB | 11 JS / 182 KiB |
/examples/feature-gallery/versions/v1.html |
36 JS / 809 KiB | 37 JS / 810 KiB |
The bytes are noise; the request is the part CLAUDE.md asks about. The layer's other two core-runtime vendor bundles are both deferred behind first use — runtime/syntax.js's hljsReady ??= import("/vendor/highlight.esm.js") and runtime/markdown.js's ready ??= import("/vendor/marked.esm.js") — and Working on the repository asks a change that adds work before presentation to "state the user-visible benefit and why that work cannot wait until after presentation", which the description doesn't.
There's no mechanical fix to suggest: deferring the import makes alignText async, and it's a public runtime/widget-api.js export, so the trade is one eager request per page against an async alignment surface. That's your call rather than mine — either resolution closes this, whether that's naming why the fetch belongs at startup or moving it behind first use.
…ule (#543) `scripts/CLAUDE.md` tells a session that a clean `git status` after `scripts/vendor.py <bundle>` is the check that a bundle still matches the script, and that the check holds only where every fetched input is pinned. It then sorts the bundles into those two classes — and two of the seven are in neither, so a session rebuilding `floating-ui` or `mcp-app` has no answer for whether a diff it sees is drift to fix or an upstream patch to take. Each belongs in a class the paragraph already describes: - `floating-ui` reproduces. `@floating-ui/dom`, `@floating-ui/core`, and `@floating-ui/utils` are all in `PINS`, and that closure is complete: `dom` depends on `core` and `utils`, `core` on `utils`, `utils` on nothing. Rebuilding it left `git status` clean. - `mcp-app` fetches an input `PINS` does not name. `@modelcontextprotocol/ext-apps@1.7.5` declares `@standard-schema/spec ^1.1.0`, which npm's resolver picks, so the bundle sits with `plot` and `pierre`. It reproduced when I rebuilt it today — which is exactly the reading the paragraph exists to keep a session from relying on. This is drift rather than a deliberate omission. The paragraph dates from `c7b37062` (2026-08-30); `mcp-app` landed in #188 on 2026-09-01 and `floating-ui` in #537 last night, and #533 — the most recent commit to touch the paragraph — added `jsdiff` to it, so it is meant to be kept current. The test follows the shape this repo already uses for instruction lists (#93, #105, #331, #496): it reads the bundle names out of `vendor.py`'s `BUILDS` and `COPIES` rather than restating them, and asserts each is named in that one paragraph. On `main` it fails with `unplaced in scripts/CLAUDE.md: ['floating-ui', 'mcp-app']`. What it deliberately does not check is *which* class a bundle is placed in — deciding that means resolving each package's dependency graph, which is a network call this suite should not make. A name in the wrong class still needs a reader to catch; a name that is absent no longer does. <details><summary>Verification</summary> - `uv run pytest tests/test_interact_layer.py` — 167 passed, 6 skipped. - The new case alone, with `scripts/CLAUDE.md` reverted to `main`'s text: `AssertionError: unplaced in scripts/CLAUDE.md: ['floating-ui', 'mcp-app']`. - `scripts/vendor.py floating-ui` and `scripts/vendor.py mcp-app` each left `git status` clean. - `npm view @floating-ui/dom@1.8.0 dependencies` → `{ '@floating-ui/core': '^1.8.0', '@floating-ui/utils': '^0.2.12' }`; `@floating-ui/core@1.8.0` → `{ '@floating-ui/utils': '^0.2.12' }`; `@floating-ui/utils@0.2.12` → none; `@modelcontextprotocol/ext-apps@1.7.5` → `{ '@standard-schema/spec': '^1.1.0' }`. </details> Co-authored-by: leaf-agent <318509791+leaf-agent@users.noreply.github.com>
Leaf's core browser runtime carried a complete Hirschberg LCS implementation even though the optional Pierre bundle already established jsdiff as a compatible dependency. This replaces the handwritten aligner with a pinned, tree-shaken jsdiff 9.0.0
diffArraysbundle while keeping Leaf's word/sentence segmentation, common-edge fallback, and 60% inline-refinement policy local.The 3,024-byte bundle rebuilds byte-for-byte. Existing fixture outputs matched exactly across word, sentence, and inline alignment, including repeated-word cases.
The static dependency adds one JavaScript request before presentation. It stays eager because
alignTextis a synchronous widget API and authored suggestions compute their word emphasis during upgrade; deferring the dependency would make that meaning arrive only after the page had presented.Tests:
uv run pytest tests/test_render_drafts.py -q -n0(81 passed);uv run pytest tests(998 passed); pre-commit hooks passed.